package authentication;

import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
import java.io.*;

import javax.security.auth.Subject;

import netTools.IPManager;
import netTools.PortManager;
import netTools.logs.RemoteLogger;
import netTools.mail.StrongMail;

import org.ietf.jgss.*;

import start.ConfigManager;
import start.StrongboxServer;
import jaxb.messages.*;

import encryption.EncryptionManager;
import virtualHD.*;

import authorization.FolderMaster;
import authorization.ResourceManager;

/**
 * 
 * @author pasquale
 * 
 * A secure server built from service credentials. It is indeed a thread which takes care of 
 * dealing with clients. Before actual communication, a secure handshaking is performed so that secret
 * keys can be exchanged and communication can be made in a secure way. During handshaking client
 * has to prove his/her identity providing kerberos credentials.
 *
 */
public class SecureServer implements Runnable,FileHandler{ 

	private static final int INFO_LOG = 0;
	private static final int WARNING_LOG = 1;
	private static final int ERR_LOG = 2;

	public SecureServer(Socket s,StrongboxServer server){
		this.userRootDir = server.USER_ROOT_DIR;
		this.socket = s;
		this.subject = server.getSubject();
		this.managers = server.getResourceManagers();
		this.logger = server.getServerLogger();
		this.logLevel = server.getLogLevel();
		this.rl = server.getRemoteLogger();
		this.mail = server.getMail();
		this.mailLog = server.logMailEnabled();
		this.mailIP = server.ipMailEnabled();
		this.encMode = server.getEncMode();

		serverOn = true;

		portManager = server.getPortManager();
		im = server.getIPManager();
	}

	@Override
	public void run(){
		try {
			inStream = new DataInputStream(socket.getInputStream());
			outStream = new DataOutputStream(socket.getOutputStream());

			writeToLogger("Got connection from client "
					+ socket.getInetAddress(),INFO_LOG);

			//privileged action with service credentials
			Subject.doAsPrivileged(subject, new PrivilegedExceptionAction<Integer>() {

				public Integer run() throws GSSException,IOException {
					GSSManager manager = GSSManager.getInstance();
					context = manager.createContext((GSSCredential)null);
					initSecureContext(context); //init secure context handshake
					return null;
				}
			},null);
			

			//Encryption Manager initialization
			if(!encMode)
				em = new EncryptionManager(context,new MessageProp(0, true),0); 
			else
				em = new EncryptionManager(context,new MessageProp(0, true),0,ConfigManager.getEncKey());
			
			//Folder Master initialization
			String username = context.getSrcName().toString().split("@")[0];

			writeToLogger("Client "+socket.getInetAddress()+" authenticated as "+username,INFO_LOG);

			FolderMaster fm = new FolderMaster(userRootDir,managers,username,this);
			
			//sends list of users
			String[] users = fm.getUsers();
			List<Resource> userList = new ArrayList<Resource>();

			for(int i=0;i<users.length;i++){
				Resource res = new Resource();
				res.setName(users[i]);
				userList.add(res);
			}
			
			Message userMess = new Message();
			userMess.setResource(userList);
			
			em.writeEncMessage(userMess.toXMLString().getBytes(), outStream);
			
			while(serverOn){
				//listens for messages
				byte[] token = em.readEncMessage(inStream);

				Message rec = new Message(new String(token));
				String op = rec.getType();

				Message response = new Message();

				try{

					//************************************* Operations ****************************************************						
					if(op.equals(FolderMaster.VIEW)){
						Resource res[] = fm.view(rec.getFolder()); //throws a VIRTUAL HDException if fails

						List<Resource> listRes = new ArrayList<Resource>();
						response.setType(Message.OK);
						for(int i=0;i<res.length;i++)
							listRes.add(res[i]);

						response.setResource(listRes);

						em.writeEncMessage(response.toXMLString().getBytes(),outStream);

					}else if(op.equals(FolderMaster.GET)){
						Resource resource = rec.getResource().get(0);
						fm.get(rec.getFolder(), resource);

					}else if(op.equals(FolderMaster.PUT)){
						Resource res = rec.getResource().get(0);
						fm.put(rec.getFolder(), res);	//throws VirtualHDException if fails

					}else if(op.equals(FolderMaster.DEL)){
						Resource res = rec.getResource().get(0);
						fm.del(rec.getFolder(), res);

						response.setType(Message.OK);
						em.writeEncMessage(response.toXMLString().getBytes(),outStream);
						
					}else if(op.equals(Message.CLOSE)){
						serverOn = false;
						writeToLogger("Client "+username+"("+socket.getInetAddress()+") closed connection",
								INFO_LOG);

					}else if(op.equals(Message.CHANGE_ROLE)){
						Folder fol = rec.getFolder();
						fm.change_role(fol);
						response.setType(Message.OK);
						em.writeEncMessage(response.toXMLString().getBytes(),outStream);
					}else 
						throw new VirtualHDException("Unrecognized command");

					//***************************************************************************************************
				}catch(VirtualHDException e){
					response.setType(Message.ERROR);
					response.setCause(e.getMessage());
					em.writeEncMessage(response.toXMLString().getBytes(),outStream);
				}
			}


			//*******************************************************************************************
		} catch (PrivilegedActionException e) {
			writeToLogger("Client "+socket.getInetAddress()+" :"+e.getException().getMessage(),WARNING_LOG);

			if(im.addToWarned(socket.getInetAddress())){ //adding this ip to warned list
				String msg = "Client "+socket.getInetAddress().getHostAddress()+" was banned";
				writeToLogger(msg,WARNING_LOG);
				if(mailIP)
					mail.sendIPAlarm(socket.getInetAddress().getHostAddress()); //sending mail
			}

		} catch (GSSException e) {
			writeToLogger("Client "+socket.getInetAddress()+" :"+e.getMessage(),WARNING_LOG);
			if(im.addToWarned(socket.getInetAddress())){ //adding this ip to warned list (true if banned)
				String msg = "Client "+socket.getInetAddress().getHostAddress()+" was banned";
				writeToLogger(msg,WARNING_LOG);
				if(mailIP)
					mail.sendIPAlarm(socket.getInetAddress().getHostAddress()); //sending mail

			}
		} catch (SocketTimeoutException e){
			writeToLogger("Connection with client "+socket.getInetAddress()+" closed due to inactivity",
					INFO_LOG);

		} catch (IOException e) {
			writeToLogger("Client "+socket.getInetAddress()+" suddently the closed connection",
					INFO_LOG);

		} finally{
			try {
				socket.close();
				context.dispose();
			} catch (IOException e) {
				e.printStackTrace();
			} catch (GSSException e) {
				e.printStackTrace();
			}
		}
	}

	//************************* file handling methods ********************************************
	@Override
	public void sendFile(InputStream stream) {
		Message portMess = new Message();
		portMess.setType(Message.OK);
		int sendPort = portManager.getPortNumber();
		portMess.setPort(sendPort);
		try {
			ServerSocket ss = new ServerSocket(sendPort);
			ss.setSoTimeout(StrongboxServer.ACCEPT_TRANSFER_TIMEOUT);
			em.writeEncMessage(portMess.toXMLString().getBytes(),outStream);
			Socket s = ss.accept();
			s.setSoTimeout(StrongboxServer.TRANSFER_TIMEOUT);
			writeToLogger("Opened a data connection to "+
					context.getSrcName().toString()+"(IP "+s.getInetAddress()+"/"+s.getPort()+") for SEND",
					INFO_LOG);

			try{
				em.secureSend(stream,s.getOutputStream());
			}catch(SocketTimeoutException e1){
				writeToLogger("Data connection versus "+s.getInetAddress()+" timed out!",INFO_LOG);

			}catch(IOException e1){
				writeToLogger("Data connection versus "+s.getInetAddress()+" failed!",INFO_LOG);

			}

			s.close();
			ss.close();

			writeToLogger("Data connection versus "+s.getInetAddress()+ " successfully closed",
					INFO_LOG);

		} catch (GSSException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}finally{
			portManager.freePort(sendPort);
		}

	}

	@Override
	public void receiveFile(OutputStream stream) {
		Message portMess = new Message();
		portMess.setType(Message.OK);
		int recPort = portManager.getPortNumber();
		portMess.setPort(recPort);
		try {
			ServerSocket ss = new ServerSocket(recPort); //waiting on a port for client connection
			ss.setSoTimeout(StrongboxServer.ACCEPT_TRANSFER_TIMEOUT);

			em.writeEncMessage(portMess.toXMLString().getBytes(),outStream);
			Socket s = ss.accept();
			s.setSoTimeout(StrongboxServer.TRANSFER_TIMEOUT);

			writeToLogger("Opened a data connection versus "+context.getSrcName().toString()+"(" +
					"IP "+s.getInetAddress()+"/"+s.getPort()+
					") for REC",INFO_LOG);

			try{
				em.secureReceive(s.getInputStream(),stream);
			}catch(GSSException e1){
				writeToLogger("Data receiving from "+s.getInetAddress()+" failed for security reasons",
						WARNING_LOG);

				if(im.addToWarned(socket.getInetAddress())){ //adding this ip to warned list
					String msg = "Client "+socket.getInetAddress().getHostAddress()+" was banned";
					writeToLogger(msg,WARNING_LOG);
					if(mailIP)
						mail.sendIPAlarm(socket.getInetAddress().getHostAddress()); //sending mail

				}
			}catch(IOException e1){
				writeToLogger("Data connection versus "+s.getInetAddress()+" failed!",
						INFO_LOG);

			}

			writeToLogger("Data connection versus "+s.getInetAddress()+ " successfully closed",
					INFO_LOG);

			s.close();
			ss.close();

		} catch (GSSException e) {

		} catch (IOException e) {
			writeToLogger("Data connection versus closed due to inactivity",
					INFO_LOG);
		} finally{
			portManager.freePort(recPort);
		}

	}

	//************************** utility methods *************************************************

	/**
	 * Makes the security context handshake
	 * @param context
	 * @return
	 * @throws GSSException
	 * @throws IOException
	 */
	private byte[] initSecureContext(GSSContext context) throws GSSException, IOException{
		byte[] token = null;

		while (!context.isEstablished()) {
			int tokenLen = inStream.readInt();
			if((tokenLen <= 0) || (tokenLen > 2048))		//2048 byes could be a more than good length
				throw new GSSException(0,tokenLen, "invalid token length received ("+tokenLen+" " +
						"bytes)");
			
			token = new byte[tokenLen];
			inStream.readFully(token);

			token = context.acceptSecContext(token, 0, token.length);

			// Send a token to the peer if one was generated by
			// acceptSecContext
			if (token != null) {
				outStream.writeInt(token.length);
				outStream.write(token);
				outStream.flush();
			}
		}
		return token;
	}
	
	/**
	 * Writes message to local log and, in case, to remote log.
	 * If writing to remote log fails sends, if enabled, a mail to
	 * admin.
	 * @param message
	 * @param type
	 */
	private void writeToLogger(String message, int type){
		try{
			switch(type){
			case INFO_LOG:
				logger.info(message);
				if(logLevel != 0)
					rl.info(message);
				break;
			case WARNING_LOG:
				logger.warning(message);
				if(logLevel != 0)
					rl.warning(message);
				break;
			case ERR_LOG:
				logger.severe(message);
				if(logLevel != 0)
					rl.severe(message);
				break;
			}

		}catch(IOException ex){
			if(mailLog)
				mail.sendLoggerAlarm();
		}
	}


	private Socket socket;
	private DataInputStream inStream;
	private DataOutputStream outStream;
	private Subject subject;

	private boolean serverOn;
	private boolean mailLog;
	private boolean mailIP;
	
	private boolean encMode;

	private GSSContext context; 
	private EncryptionManager em;
	private StrongMail mail;

	private ResourceManager[] managers;

	private PortManager portManager;
	private Logger logger;
	private int logLevel;
	private RemoteLogger rl;
	private IPManager im;
	
	private String userRootDir;

}
